/* 
 * AUTHOR: Kevin Lam
 */

package com.apps.services;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import com.apps.ubc.cc.model.AmazonBookModel;

public class AmazonWebService {

	private static final String AWS_ACCESS_KEY_ID = "AKIAI2EIYYQQFCWV6XCA";
	private static final String AWS_SECRET_KEY = "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";
	private static final String ASSOCIATE_TAG_CA = "ubcoco03-20";
	private static final String ECS_CA = "ecs.amazonaws.ca";

	private static final int ISBN_LOOKUP = 0x1;
	private static final int PRICE_LOOKUP = 0x2;
	private static final int IMAGE_LOOKUP = 0x3;

	private static final int FETCH_ASIN = 0x1;
	private static final int FETCH_TITLE = 0x2;
	private static final int FETCH_DETAIL = 0x3;
	private static final int FETCH_USED_PRICE = 0x4;
	private static final int FETCH_NEW_PRICE = 0x5;
	private static final int FETCH_IMAGE = 0x6;
	
	private SignedRequestsHelper helper;
	
	public AmazonWebService(){
		try {
			helper = SignedRequestsHelper.getInstance(ECS_CA,
					AWS_ACCESS_KEY_ID, AWS_SECRET_KEY);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public AmazonBookModel search(String keyword) {
		String requestURL = lookUpKeyword(keyword);
		String title = fetch(requestURL, FETCH_TITLE);
		String detail = fetch(requestURL, FETCH_DETAIL);
		String asin = fetch(requestURL, FETCH_ASIN);
		requestURL = lookUpPrice(asin,PRICE_LOOKUP);
		String priceNew = fetch(requestURL, FETCH_NEW_PRICE);
		String priceUsed = fetch(requestURL, FETCH_USED_PRICE);
		requestURL = lookUpImage(asin);
		String imageUrl = fetch(requestURL, FETCH_IMAGE);
		AmazonBookModel abm = new AmazonBookModel(asin, title, imageUrl,
				detail, priceNew, priceUsed);
		return abm;
	}

	private  String lookUpKeyword(String keywords) {
		String requestUrl = null;
		Map<String, String> params = getLookupMap(ISBN_LOOKUP);
		params.put("Keywords", keywords);
		if (helper != null)
			requestUrl = helper.sign(params);
		return requestUrl;
	}

	private String lookUpPrice(String asin, int type) {
		String requestUrl = null;
		Map<String, String> params = getLookupMap(type);
		params.put("ItemId", asin);
		if (helper != null)
			requestUrl = helper.sign(params);
		return requestUrl;
	}

	private  String lookUpImage(String asin) {
		String requestUrl = null;
		Map<String, String> params = getLookupMap(IMAGE_LOOKUP);
		params.put("ItemId", asin);
		if (helper != null)
			requestUrl = helper.sign(params);
		return requestUrl;
	}

	private String fetch(String requestUrl, int type) {
		String result = "";
		if(requestUrl == null || requestUrl.equals(""))
			return "";
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document doc = db.parse(requestUrl);
			Node node = null;
			switch (type) {
			case FETCH_ASIN:
				node = doc.getElementsByTagName("ASIN").item(0);
				break;
			case FETCH_DETAIL:
				node = doc.getElementsByTagName("DetailPageURL").item(0);
				break;
			case FETCH_IMAGE:
				Node parentNode = doc.getElementsByTagName("SmallImage")
						.item(0);
				if (parentNode != null)
					node = parentNode.getChildNodes().item(0);
				break;
			case FETCH_NEW_PRICE:
				Node lowestNewPriceNode = doc.getElementsByTagName(
						"LowestNewPrice").item(0);
				if (lowestNewPriceNode != null)
					node = lowestNewPriceNode.getLastChild();
				break;
			case FETCH_USED_PRICE:
				Node offerListingIdNode = doc.getElementsByTagName(
						"OfferListingId").item(0);
				if (offerListingIdNode != null)
					node = offerListingIdNode.getNextSibling().getLastChild();
				break;
			case FETCH_TITLE:
				node = doc.getElementsByTagName("Title").item(0);
				break;
			}
			if (node != null)
				result = node.getTextContent();

		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return result;
	}

	private HashMap<String, String> getLookupMap(int mapEnum) {

		HashMap<String, String> params = new HashMap<String, String>();
		params.put("AssociateTag", ASSOCIATE_TAG_CA);
		params.put("Service", "AWSECommerceService");
		params.put("Version", "2009-03-31");
		switch (mapEnum) {
		case ISBN_LOOKUP:
			params.put("IdType", "ISBN");
			params.put("SearchIndex", "Books");
			params.put("Operation", "ItemSearch");
			break;
		case PRICE_LOOKUP:
			params.put("IdType", "ASIN");
			params.put("Operation", "ItemLookup");
			params.put("ResponseGroup", "Offers");
			params.put("Condition", "Used");
			break;
		case IMAGE_LOOKUP:
			params.put("IdType", "ASIN");
			params.put("Operation", "ItemLookup");
			params.put("ResponseGroup", "Images");
			params.put("Condition", "All");
			break;
		}
		return params;
	}

	// SignedRequestHelper taken from Amazon documentation
	 static class SignedRequestsHelper {
		/**
		 * All strings are handled as UTF-8
		 */
		private static final String UTF8_CHARSET = "UTF-8";

		/**
		 * The HMAC algorithm required by Amazon
		 */
		private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";

		/**
		 * This is the URI for the service, don't change unless you really know
		 * what you're doing.
		 */
		private static final String REQUEST_URI = "/onca/xml";

		/**
		 * The sample uses HTTP GET to fetch the response. If you changed the
		 * sample to use HTTP POST instead, change the value below to POST.
		 */
		private static final String REQUEST_METHOD = "GET";

		private String endpoint = null;
		private String awsAccessKeyId = null;
		private String awsSecretKey = null;

		private SecretKeySpec secretKeySpec = null;
		private Mac mac = null;

		/**
		 * You must provide the three values below to initialize the helper.
		 * 
		 * @param endpoint
		 *            Destination for the requests.
		 * @param awsAccessKeyId
		 *            Your AWS Access Key ID
		 * @param awsSecretKey
		 *            Your AWS Secret Key
		 */
		public static SignedRequestsHelper getInstance(String endpoint,
				String awsAccessKeyId, String awsSecretKey)
				throws IllegalArgumentException, UnsupportedEncodingException,
				NoSuchAlgorithmException, InvalidKeyException {
			if (null == endpoint || endpoint.length() == 0) {
				throw new IllegalArgumentException("endpoint is null or empty");
			}
			if (null == awsAccessKeyId || awsAccessKeyId.length() == 0) {
				throw new IllegalArgumentException(
						"awsAccessKeyId is null or empty");
			}
			if (null == awsSecretKey || awsSecretKey.length() == 0) {
				throw new IllegalArgumentException(
						"awsSecretKey is null or empty");
			}

			SignedRequestsHelper instance = new SignedRequestsHelper();
			instance.endpoint = endpoint.toLowerCase();
			instance.awsAccessKeyId = awsAccessKeyId;
			instance.awsSecretKey = awsSecretKey;

			byte[] secretyKeyBytes = instance.awsSecretKey
					.getBytes(UTF8_CHARSET);
			instance.secretKeySpec = new SecretKeySpec(secretyKeyBytes,
					HMAC_SHA256_ALGORITHM);
			instance.mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
			instance.mac.init(instance.secretKeySpec);

			return instance;
		}

		/**
		 * The construct is private since we'd rather use getInstance()
		 */
		private SignedRequestsHelper() {
		}

		/**
		 * This method signs requests in hashmap form. It returns a URL that
		 * should be used to fetch the response. The URL returned should not be
		 * modified in any way, doing so will invalidate the signature and
		 * Amazon will reject the request.
		 */
		public String sign(Map<String, String> params) {
			// Let's add the AWSAccessKeyId and Timestamp parameters to the
			// request.
			params.put("AWSAccessKeyId", this.awsAccessKeyId);
			params.put("Timestamp", this.timestamp());

			// The parameters need to be processed in lexicographical order, so
			// we'll
			// use a TreeMap implementation for that.
			SortedMap<String, String> sortedParamMap = new TreeMap<String, String>(
					params);

			// get the canonical form the query string
			String canonicalQS = this.canonicalize(sortedParamMap);

			// create the string upon which the signature is calculated
			String toSign = REQUEST_METHOD + "\n" + this.endpoint + "\n"
					+ REQUEST_URI + "\n" + canonicalQS;

			// get the signature
			String hmac = this.hmac(toSign);
			String sig = this.percentEncodeRfc3986(hmac);

			// construct the URL
			String url = "http://" + this.endpoint + REQUEST_URI + "?"
					+ canonicalQS + "&Signature=" + sig;

			return url;
		}

		/**
		 * This method signs requests in query-string form. It returns a URL
		 * that should be used to fetch the response. The URL returned should
		 * not be modified in any way, doing so will invalidate the signature
		 * and Amazon will reject the request.
		 */
		public String sign(String queryString) {
			// let's break the query string into it's constituent name-value
			// pairs
			Map<String, String> params = this.createParameterMap(queryString);

			// then we can sign the request as before
			return this.sign(params);
		}

		/**
		 * Compute the HMAC.
		 * 
		 * @param stringToSign
		 *            String to compute the HMAC over.
		 * @return base64-encoded hmac value.
		 */
		private String hmac(String stringToSign) {
			String signature = null;
			byte[] data;
			byte[] rawHmac;
			try {
				data = stringToSign.getBytes(UTF8_CHARSET);
				rawHmac = mac.doFinal(data);
				Base64 encoder = new Base64();
				signature = new String(encoder.encode(rawHmac));
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException(UTF8_CHARSET + " is unsupported!", e);
			}
			return signature;
		}

		/**
		 * Generate a ISO-8601 format timestamp as required by Amazon.
		 * 
		 * @return ISO-8601 format timestamp.
		 */
		private String timestamp() {
			String timestamp = null;
			Calendar cal = Calendar.getInstance();
			DateFormat dfm = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
			dfm.setTimeZone(TimeZone.getTimeZone("GMT"));
			timestamp = dfm.format(cal.getTime());
			return timestamp;
		}

		/**
		 * Canonicalize the query string as required by Amazon.
		 * 
		 * @param sortedParamMap
		 *            Parameter name-value pairs in lexicographical order.
		 * @return Canonical form of query string.
		 */
		private String canonicalize(SortedMap<String, String> sortedParamMap) {
			if (sortedParamMap.isEmpty()) {
				return "";
			}

			StringBuffer buffer = new StringBuffer();
			Iterator<Map.Entry<String, String>> iter = sortedParamMap
					.entrySet().iterator();

			while (iter.hasNext()) {
				Map.Entry<String, String> kvpair = iter.next();
				buffer.append(percentEncodeRfc3986(kvpair.getKey()));
				buffer.append("=");
				buffer.append(percentEncodeRfc3986(kvpair.getValue()));
				if (iter.hasNext()) {
					buffer.append("&");
				}
			}
			String cannoical = buffer.toString();
			return cannoical;
		}

		/**
		 * Percent-encode values according the RFC 3986. The built-in Java
		 * URLEncoder does not encode according to the RFC, so we make the extra
		 * replacements.
		 * 
		 * @param s
		 *            decoded string
		 * @return encoded string per RFC 3986
		 */
		private String percentEncodeRfc3986(String s) {
			String out;
			try {
				out = URLEncoder.encode(s, UTF8_CHARSET).replace("+", "%20")
						.replace("*", "%2A").replace("%7E", "~");
			} catch (UnsupportedEncodingException e) {
				out = s;
			}
			return out;
		}

		/**
		 * Takes a query string, separates the constituent name-value pairs and
		 * stores them in a hashmap.
		 * 
		 * @param queryString
		 * @return
		 */
		private Map<String, String> createParameterMap(String queryString) {
			Map<String, String> map = new HashMap<String, String>();
			String[] pairs = queryString.split("&");

			for (String pair : pairs) {
				if (pair.length() < 1) {
					continue;
				}

				String[] tokens = pair.split("=", 2);
				for (int j = 0; j < tokens.length; j++) {
					try {
						tokens[j] = URLDecoder.decode(tokens[j], UTF8_CHARSET);
					} catch (UnsupportedEncodingException e) {
					}
				}
				switch (tokens.length) {
				case 1: {
					if (pair.charAt(0) == '=') {
						map.put("", tokens[0]);
					} else {
						map.put(tokens[0], "");
					}
					break;
				}
				case 2: {
					map.put(tokens[0], tokens[1]);
					break;
				}
				}
			}
			return map;
		}
	}

}
